/*
 * Galaxium Messenger
 * 
 * Copyright (C) 2008 Paul Burton <paulburton89@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.Collections.Generic;

using Anculus.Core;

using Galaxium.Core;

namespace Galaxium.Protocol.Msn
{
	public enum MsnP2PSessionState { Error, WaitingForLocal, WaitingForRemote, Active, Closing, Closed }

	public partial class MsnP2PSession : IDisposable
	{
		// The period of inactivity before we'll close the session
		const int _timeout = 120000;
		
		public event EventHandler					Activated;
		public event EventHandler<EntityEventArgs>	Closing;
		public event EventHandler<EntityEventArgs>	Closed;
		public event EventHandler					Error;
		public event EventHandler					WaitingForLocal;
		public event EventHandler					WaitingForRemote;
		
		IMsnP2PApplication _app;
		IMsnP2PBridge _bridge;
		SLPRequestMessage _invite;
		MsnAccount _local;
		MsnContact _remote;
		MsnSession _session;
		MsnP2PSessionState _state = MsnP2PSessionState.Closed;
		uint _sessionID;
		uint _localID;
		uint _localBaseID;
		uint _remoteID;
		uint _remoteBaseID;
		uint _timeoutTimer;
		
		static Random _random = new Random ();

		public IMsnP2PApplication Application
		{
			get { return _app; }
		}
		
		public IMsnP2PBridge Bridge
		{
			get { return _bridge; }
		}
		
		public SLPRequestMessage Invite
		{
			get { return _invite; }
		}
		
		public MsnAccount Local
		{
			get { return _local; }
		}
		
		public MsnContact Remote
		{
			get { return _remote; }
		}
		
		public uint LocalID
		{
			get { return _localID; }
			set { _localID = value; }
		}
		
		public uint RemoteID
		{
			get { return _remoteID; }
		}
		
		public uint LocalBaseID
		{
			get { return _localBaseID; }
		}
		
		public uint RemoteBaseID
		{
			get { return _remoteBaseID; }
		}
		
		public MsnSession Session
		{
			get { return _session; }
		}
		
		public uint SessionID
		{
			get { return _sessionID; }
		}
		
		public MsnP2PSessionState State
		{
			get { return _state; }
		}

		public MsnP2PSession (IMsnP2PApplication app)
		{
			_app = app;
			_local = app.Local;
			_remote = app.Remote;
			_session = _local.Session;
			_sessionID = (uint)_random.Next (10000, int.MaxValue);
			
			_localBaseID = (uint)_random.Next (10000, int.MaxValue);
			_localID = _localBaseID;
			
			_app.P2PSession = this;
			
			_remote.DirectBridgeEstablished += RemoteDirectBridgeEstablished;
			
			Log.Debug ("P2PSession {0} created (Initiated locally)", _sessionID);
			
			_invite = new SLPRequestMessage (_remote, "INVITE");
			_invite.ContentType = "application/x-msnmsgr-sessionreqbody";
			_invite.MIMEBody["EUF-GUID"] = MsnP2PUtility.GetEufGuid (_app).ToString ("B").ToUpperInvariant ();
			_invite.MIMEBody["SessionID"] = _sessionID.ToString ();
			_invite.MIMEBody["AppID"] = _app.AppID.ToString ();
			_invite.MIMEBody["Context"] = _app.CreateInviteContext ();
			
			OnWaitingForRemote ();
			
			Send (_invite, delegate (P2PMessage ack)
			{
				_remoteBaseID = ack.Header.MessageID;
				_remoteID = _remoteBaseID;
			});
		}
		
		public MsnP2PSession (SLPRequestMessage invite, P2PMessage msg)
		{
			_invite = invite;
			
			_session = _invite.Session;
			_local = _invite.To as MsnAccount;
			_remote = _invite.From as MsnContact;
			
			_localBaseID = (uint)_random.Next (10000, int.MaxValue);
			_localID = _localBaseID;
			_remoteBaseID = (msg != null) ? msg.Header.MessageID : 0;
			_remoteID = _remoteBaseID;
			
			_remote.DirectBridgeEstablished += RemoteDirectBridgeEstablished;
			
			if (!uint.TryParse (_invite.MIMEBody["SessionID"].Value, out _sessionID))
				Log.Warn ("Unable to parse invite SessionID");
			
			Log.Debug ("P2PSession {0} created (Initiated remotely)", _sessionID);
			
			// Send Base ID
			
			if (msg != null)
				Send (msg.CreateAck ());
			
			// Initialize P2P Application
			
			Type appType = MsnP2PUtility.GetApp (new Guid (_invite.MIMEBody["EUF-GUID"].Value));
			
			if (appType != null)
				_app = Activator.CreateInstance (appType, this) as IMsnP2PApplication;
			else
			{
				Log.Warn ("Unknown app for EUF-GUID {0}", _invite.MIMEBody["EUF-GUID"].Value);
				Log.Debug ("Invite:\n{0}", invite);
			}
			
			if (!_app.CheckInvite (_invite))
			{
				Log.Warn ("P2PSession {0} app rejects invite\n{1}", _sessionID, invite);
				
				OnError ();
				
				SLPStatusMessage slp = new SLPStatusMessage (_remote, 500, "Internal Server Error");
				slp.Branch = _invite.Branch;
				slp.CallID = _invite.CallID;
				slp.ContentType = "application/x-msnmsgr-sessionreqbody";
				slp.MIMEBody["SessionID"] = _sessionID.ToString ();
				
				Send (slp, delegate
				{
					Close ();
				});
				
				return;
			}
			
			// Set this here so Accept doesn't complain
			_state = MsnP2PSessionState.WaitingForLocal;
			
			if (_app.AutoAccept)
				Accept ();
			else
				OnWaitingForLocal ();
		}
		
		public void Dispose ()
		{
			_remote.DirectBridgeEstablished -= RemoteDirectBridgeEstablished;
			
			DisposeApp ();
			Migrate (null);
		}
		
		void RemoteDirectBridgeEstablished (object sender, EventArgs args)
		{
			MigrateToOptimalBridge ();
		}
		
		void MigrateToOptimalBridge ()
		{
			Log.Debug ("P2PSession {0} choosing optimal bridge", _sessionID);
			
			if ((_remote.DirectBridge != null) && _remote.DirectBridge.Open)
			{
				Migrate (_remote.DirectBridge);
				return;
			}
			
			Migrate (MsnP2PUtility.GetBridge (this));
		}
		
		void Migrate (IMsnP2PBridge newBridge)
		{
			Log.Debug ("P2PSession {0} migrating from bridge {1} to {2}", _sessionID,
			           (_bridge != null) ? _bridge.ToString () : "null",
			           (newBridge != null) ? newBridge.ToString () : "null");
			
			if (_bridge != null)
			{
				_bridge.BridgeOpened -= BridgeOpened;
				_bridge.BridgeClosed -= BridgeClosed;
				_bridge.BridgeSent -= BridgeSent;
				
				_bridge.MigrateQueue (this, newBridge);
			}
			
			_bridge = newBridge;
			
			if (_bridge != null)
			{
				_bridge.BridgeOpened += BridgeOpened;
				_bridge.BridgeClosed += BridgeClosed;
				_bridge.BridgeSent += BridgeSent;
				
				if ((_directNegotiationTimer != 0) && !(_bridge is SBBridge))
					DirectNegotiationSuccessful ();
			}
			else
				UnmapDirectPort ();
		}
		
		public void Accept ()
		{
			if (_state != MsnP2PSessionState.WaitingForLocal)
			{
				Log.Warn ("Accept called, but we're not waiting for the local client (State {0})", _state);
				return;
			}
			
			Log.Debug ("P2PSession {0} accepted", _sessionID);
			
			SLPStatusMessage slp = new SLPStatusMessage (_remote, 200, "OK");
			slp.Branch = _invite.Branch;
			slp.CallID = _invite.CallID;
			slp.ContentType = "application/x-msnmsgr-sessionreqbody";
			slp.MIMEBody["SessionID"] = _sessionID.ToString ();
			
			Send (slp, delegate
			{
				OnActive ();
				
				if (_app != null)
					_app.Begin ();
				else
					Log.Warn ("Unable to begin p2p application, object is null!");
			});
		}
		
		public void Decline ()
		{
			if (_state != MsnP2PSessionState.WaitingForLocal)
			{
				Log.Warn ("Declined called, but we're not waiting for the local client");
				return;
			}
			
			Log.Debug ("P2PSession {0} declined", _sessionID);
			
			SLPStatusMessage slp = new SLPStatusMessage (_remote, 603, "Decline");
			slp.Branch = _invite.Branch;
			slp.CallID = _invite.CallID;
			slp.ContentType = "application/x-msnmsgr-sessionreqbody";
			slp.MIMEBody["SessionID"] = _sessionID.ToString ();
			
			Send (slp, delegate
			{
				Close ();
			});
		}
		
		public void Close ()
		{
			if (_state == MsnP2PSessionState.Closing)
			{
				Log.Warn ("P2PSession {0} was already closing, forcing unclean closure", _sessionID);
				
				OnClosed (new EntityEventArgs (Local));
			}
			else
			{
				OnClosing (new EntityEventArgs (Local));
				
				SLPMessage slp = new SLPRequestMessage (_remote, "BYE");
				slp.CallID = _invite.CallID;
				slp.ContentType = "application/x-msnmsgr-sessionclosebody";
				slp.MIMEBody["SessionID"] = _sessionID.ToString ();
				
				P2PMessage msg = new P2PMessage (_session);
				msg.SLPMessage = slp;
				
				Send (msg, delegate
				{
					OnClosed (new EntityEventArgs (Local));
				});
			}
		}
		
		public bool ProcessMessage (IMsnP2PBridge bridge, P2PMessage msg)
		{
			ResetTimeoutTimer ();
			
			_remoteID = msg.Header.MessageID;
			
			if ((msg.Header.Flags & P2PHeaderFlag.Waiting) == P2PHeaderFlag.Waiting)
			{
				//TODO: what should we do with these?
				return true;
			}
			
			if ((_state == MsnP2PSessionState.Closed) || (_state == MsnP2PSessionState.Error))
			{
				Log.Warn ("P2PSession {0} received message whilst in '{1}' state", _sessionID, _state);
				return false;
			}
			
			if (msg.SLPMessage != null)
			{
				if (msg.SLPMessage is SLPRequestMessage)
				{
					SLPRequestMessage req = msg.SLPMessage as SLPRequestMessage;
					
					if ((req.ContentType == "application/x-msnmsgr-sessionclosebody") && (req.Method == "BYE"))
					{
						P2PMessage byeAck = msg.CreateAck ();
						byeAck.Header.Flags = P2PHeaderFlag.CloseSession;
						
						Send (byeAck);
						
						OnClosed (new EntityEventArgs (Remote));
						
						return true;
					}
					else
					{
						Send (msg.CreateAck ());
						
						if ((req.ContentType == "application/x-msnmsgr-transreqbody")
						    || (req.ContentType == "application/x-msnmsgr-transrespbody"))
						{
							// Direct connection invite
							ProcessDirectInvite (msg);
							
							return true;
						}
						else if (req.Method == "ACK")
							return true;
					}
				}
				else if (msg.SLPMessage is SLPStatusMessage)
				{
					SLPStatusMessage status = msg.SLPMessage as SLPStatusMessage;
					
					Send (msg.CreateAck ());
					
					if (status.Code == 200) // OK
					{
						if (status.ContentType == "application/x-msnmsgr-transrespbody")
							ProcessDirectInvite (msg);
						else
						{
							OnActive ();
							
							_app.Begin ();
						}
						
						return true;
					}
					else if (status.Code == 603) // Decline
					{
						OnClosed (new EntityEventArgs (Remote));
						
						return true;
					}
				}
				
				Log.Warn ("Unhandled SLP Message:\n{0}", msg.ToString ());
				
				OnError ();
				
				return true;
			}
			
			if (_app == null)
			{
				Log.Warn ("P2PSession {0}: Received message for P2P app, but it's either been disposed or not created", _sessionID);
				return false;
			}
			
			return _app.ProcessMessage (bridge, msg);
		}
		
		public void Send (P2PMessage msg, AckHandler ackHandler)
		{
			ResetTimeoutTimer ();
			
			if (msg.Header.MessageID == 0)
				msg.Header.MessageID = NextID ();
			
			MsnP2PUtility.RegisterAckHandler (msg, ackHandler);
			
			if (_bridge == null)
				MigrateToOptimalBridge ();
			
			_bridge.Send (this, Remote, msg);
		}
		
		public void Send (P2PMessage msg)
		{
			Send (msg, null);
		}
		
		public void Send (SLPMessage slp, AckHandler ackHandler)
		{
			P2PMessage msg = new P2PMessage (_session);
			msg.SLPMessage = slp;
			
			Send (msg, ackHandler);
		}
		
		public void Send (SLPMessage slp)
		{
			Send (slp, null);
		}
		
		void BridgeOpened (object sender, EventArgs args)
		{
			if (_bridge.Ready (this) && (_app != null))
				_app.BridgeIsReady ();
		}
		
		void BridgeClosed (object sender, EventArgs args)
		{
			if (_remote.Presence == MsnPresence.Offline)
				OnClosed (new EntityEventArgs (_remote));
			else
				MigrateToOptimalBridge ();
		}
		
		void BridgeSent (object sender, P2PMessageEventArgs args)
		{
			if (_bridge.Ready (this) && (_app != null))
				_app.BridgeIsReady ();
		}
		
		public uint NextID ()
		{
			if (_localID == _localBaseID)
				_localID++;
			
			return _localID++;
		}
		
		protected virtual void OnClosing (EntityEventArgs args)
		{
			Log.Debug ("P2PSession {0} closing", _sessionID);
			
			_state = MsnP2PSessionState.Closing;
			
			if (Closing != null)
				Closing (this, args);
			
			DisposeApp ();
		}
		
		protected virtual void OnClosed (EntityEventArgs args)
		{
			Log.Debug ("P2PSession {0} closed", _sessionID);
			
			_state = MsnP2PSessionState.Closed;
			
			if (_timeoutTimer != 0)
			{
				TimerUtility.RemoveCallback (_timeoutTimer);
				_timeoutTimer = 0;
			}
			
			if (Closed != null)
				Closed (this, args);
			
			DisposeApp ();
		}
		
		protected virtual void OnError ()
		{
			Log.Error ("P2PSession {0} error", _sessionID);
			
			_state = MsnP2PSessionState.Error;
			
			if (Error != null)
				Error (this, EventArgs.Empty);
			
			DisposeApp ();
		}
		
		protected virtual void OnWaitingForLocal ()
		{
			Log.Debug ("P2PSession {0} waiting for local", _sessionID);
			
			_state = MsnP2PSessionState.WaitingForLocal;
			
			if (WaitingForLocal != null)
				WaitingForLocal (this, EventArgs.Empty);
		}
		
		protected virtual void OnWaitingForRemote ()
		{
			Log.Debug ("P2PSession {0} waiting for remote", _sessionID);
			
			_state = MsnP2PSessionState.WaitingForRemote;
			
			if (WaitingForRemote != null)
				WaitingForRemote (this, EventArgs.Empty);
		}
		
		protected virtual void OnActive ()
		{
			Log.Debug ("P2PSession {0} active", _sessionID);
			
			_state = MsnP2PSessionState.Active;
			
			if (Activated != null)
				Activated (this, EventArgs.Empty);
		}
		
		void DisposeApp ()
		{
			if (_app != null)
			{
				_app.Dispose ();
				_app = null;
			}
		}
		
		void ResetTimeoutTimer ()
		{
			if (_timeoutTimer != 0)
			{
				TimerUtility.ResetCallback (_timeoutTimer);
				return;
			}
			
			_timeoutTimer = TimerUtility.RequestInfiniteCallback (delegate
			{
				Log.Debug ("P2PSession {0} timed out through inactivity", _sessionID);
				
				Close ();
			}, _timeout);
		}
	}
}
